﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Nwc.XmlRpc;
using ExtensionLoader;
using ExtensionLoader.Config;
using HttpServer;
using OpenMetaverse;
using OpenMetaverse.Http;
using OpenMetaverse.StructuredData;
using CableBeachMessages;
using OpenSimDbLinq;

namespace WorldServer.Extensions
{
    public class OpenSimLogin : IExtension<WorldServer>
    {
        protected WorldServer server;
        protected string defaultWelcomeMessage;
        protected int minimumLoginLevel = 0;
        protected OpenSimDatabase userDatabase;

        public OpenSimLogin()
        {
        }

        public virtual bool Start(WorldServer server)
        {
            this.server = server;

            if (!LoadConfiguration())
                return false;

            server.XmlRpcProvider.AddXmlRpcHandler(@"^/$", "login_to_simulator", LoginMethod);

            return true;
        }

        protected bool LoadConfiguration()
        {
            try
            {
                IConfig userdbConfig = server.ConfigFile.Configs["OpenSimUserDatabase"];
                IConfig loginConfig = server.ConfigFile.Configs["OpenSimLogin"];
                userDatabase = OpenSimUtils.LoadDatabaseFromConfig(userdbConfig);

                try
                {
                    // DEBUG:
                    //userDatabase.Log = Console.Out;

                    // Run a query to select the the first user in the database to confirm
                    // that database access is working
                    var creationDateQuery = from user in userDatabase.Users select user;
                    var result = creationDateQuery.SingleOrDefault();

                    if (result != null)
                        Logger.Info("[OpenSimLogin] Connected to user database");
                    else
                        Logger.Warn("[OpenSimLogin] Connected to an empty user database");
                }
                catch (Exception ex)
                {
                    Logger.Error("[OpenSimLogin] Failed to establish a database connection: " + ex.Message);
                    return false;
                }

                // Load the default welcome message
                defaultWelcomeMessage = loginConfig.GetString("DefaultWelcomeMessage");
            }
            catch (Exception)
            {
                Logger.Error("[OpenSimLogin] Failed to load [OpenSimDatabase] or [OpenSimLogin] section from " + WorldServer.CONFIG_FILE);
                return false;
            }
            return true;
        }

        public void Stop()
        {
            if (userDatabase != null)
                userDatabase.Dispose();
        }

        protected XmlRpcResponse LoginMethod(XmlRpcRequest request, IHttpRequest httpRequest)
        {
            UUID sessionID;
            Hashtable requestData = (Hashtable)request.Params[0];

            UUID avatarID;
            string firstName = (string)requestData["first"];
            string lastName = (string)requestData["last"];
            string passHash = (string)requestData["passwd"];

            if (!String.IsNullOrEmpty(passHash))
            {
                passHash = passHash.Replace("$1$", String.Empty);
                passHash = Utils.MD5String(passHash + ":");

                // Look up these credentials in the database
                Users dbUser = userDatabase.Users.SingleOrDefault(user =>
                    user.UserName == firstName &&
                    user.LastName == lastName &&
                    user.PasswordHash == passHash);

                if (dbUser != null)
                {
                    // Check if this avatar has a high enough login level to login right now
                    if (dbUser.GodLevel >= minimumLoginLevel)
                    {
                        Uri identity = new Uri(server.HttpUri, "/users/" + firstName + "." + lastName);

                        #region Attributes

                        // Get the current attributes (if any) for this avatar from the AttributeProvider
                        Dictionary<Uri, OSD> attributes = server.AttributeProvider.GetAttributes(identity);

                        // Convert the OpenSim database users row to attributes
                        DatabaseRowToAttributes(dbUser, ref attributes, out avatarID);

                        // Synchronize the attributes back to the AttributeProvider
                        server.AttributeProvider.UpdateAttributes(identity, attributes);

                        #endregion Attributes

                        #region Discovery

                        // Create an empty list of services, since the client can't specify its
                        // own services through the XML-RPC login
                        ServiceCollection services = new ServiceCollection();

                        // Get a list of services to attach to this avatar. This will also contact any trusted
                        // services to attempt capability fetching
                        server.ServiceProvider.GetServices(identity, true, ref services);

                        // Since the Linden viewer has no way of assisting in service discovery or performing
                        // an OAuth handshake, any missing service capabilities at this point means a failure
                        foreach (Service service in services.Values)
                        {
                            foreach (KeyValuePair<Uri, Uri> serviceMethod in service.Capabilities)
                            {
                                if (serviceMethod.Value == null)
                                {
                                    Logger.Error("[OpenSimLogin] Login failed for " + firstName + " " + lastName +
                                        ", missing service endpoint for capability " + serviceMethod.Key);
                                    return LindenLoginHelper.CreateLoginServicesErrorResponse();
                                }
                            }
                        }

                        #endregion Discovery

                        #region Session Management

                        // Check if this avatar is already logged in. If so, destroy the old session
                        if (server.SessionProvider.TryGetSession(identity, out sessionID))
                            LogoutAvatar(sessionID);

                        // Create an avatar object
                        Avatar avatar = new Avatar(identity, avatarID, attributes, services);

                        // Create a new session
                        sessionID = server.SessionProvider.CreateSession(avatar);
                        Logger.Info("[OpenSimLogin] Session created for " + identity);

                        // Update last login timestamp
                        avatar.Attributes[AvatarAttributes.LAST_LOGIN_DATE] = OSD.FromDate(DateTime.Now);
                        server.AttributeProvider.UpdateAttributes(avatar.Identity, avatar.Attributes);

                        #endregion Session Management

                        // Return the XML-RPC login response
                        return LindenLoginHelper.LoginMethod(server, request, httpRequest, avatar, sessionID, defaultWelcomeMessage);
                    }
                    else
                    {
                        // Current minimum login level is higher than this avatars login level
                        Logger.Info("[OpenSimLogin] Login blocked for " + firstName + " " + lastName + " with a login level of " + dbUser.GodLevel);
                        return LindenLoginHelper.CreateLoginBlockedResponse();
                    }
                }
                else
                {
                    Logger.Info("[OpenSimLogin] Login failed for " + firstName + " " + lastName);
                }
            }
            else
            {
                Logger.Info("[OpenSimLogin] No password provided during login from " + firstName + " " + lastName);
            }

            return LindenLoginHelper.CreateLoginFailedResponse();
        }

        protected void LogoutAvatar(UUID authToken)
        {
            // FIXME: Notify simulators this avatar is logging out?
            server.SessionProvider.RemoveSession(authToken);
        }

        void DatabaseRowToAttributes(Users dbUser, ref Dictionary<Uri, OSD> attributes, out UUID avatarID)
        {
            UUID.TryParse(dbUser.UUID, out avatarID);
            Vector3 homeLookAt = new Vector3(dbUser.HomeLookAtX, dbUser.HomeLookAtY, dbUser.HomeLookAtZ);
            Vector3 homePosition = new Vector3(dbUser.HomeLocationX, dbUser.HomeLocationY, dbUser.HomeLocationZ);
            uint homeRegionX, homeRegionY;
            Utils.LongToUInts((ulong)dbUser.HomeRegion, out homeRegionX, out homeRegionY);
            homeRegionX /= 256;
            homeRegionY /= 256;

            attributes[AvatarAttributes.AVATAR_ID] = OSD.FromUUID(avatarID);

            if (!attributes.ContainsKey(AvatarAttributes.BIOGRAPHY)) attributes[AvatarAttributes.BIOGRAPHY] = OSD.FromString(dbUser.ProfileAboutText);
            if (!attributes.ContainsKey(AvatarAttributes.BIRTH_DATE)) attributes[AvatarAttributes.BIRTH_DATE] = OSD.FromDate(Utils.UnixTimeToDateTime(dbUser.Created));
            if (!attributes.ContainsKey(AvatarAttributes.CAN_DO)) attributes[AvatarAttributes.CAN_DO] = OSD.FromUInteger(dbUser.ProfileCanDoMask);
            if (!attributes.ContainsKey(AvatarAttributes.CUSTOM_TYPE)) attributes[AvatarAttributes.CUSTOM_TYPE] = OSD.FromString(dbUser.CustomType);
            if (!attributes.ContainsKey(AvatarAttributes.EMAIL)) attributes[AvatarAttributes.EMAIL] = OSD.FromString(dbUser.Email);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_LIFE_BIOGRAPHY)) attributes[AvatarAttributes.FIRST_LIFE_BIOGRAPHY] = OSD.FromString(dbUser.ProfileFirstText);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_LIFE_IMAGE_ID)) attributes[AvatarAttributes.FIRST_LIFE_IMAGE_ID] = OSD.FromString(dbUser.ProfileFirstImage);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_NAME)) attributes[AvatarAttributes.FIRST_NAME] = OSD.FromString(dbUser.UserName);
            if (!attributes.ContainsKey(AvatarAttributes.GOD_LEVEL)) attributes[AvatarAttributes.GOD_LEVEL] = OSD.FromInteger(dbUser.GodLevel);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_LOOKAT)) attributes[AvatarAttributes.HOME_LOOKAT] = OSD.FromVector3(homeLookAt);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_POSITION)) attributes[AvatarAttributes.HOME_POSITION] = OSD.FromVector3(homePosition);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_ID)) attributes[AvatarAttributes.HOME_REGION_ID] = OSD.FromString(dbUser.HomeRegionID);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_X)) attributes[AvatarAttributes.HOME_REGION_X] = OSD.FromUInteger(homeRegionX);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_Y)) attributes[AvatarAttributes.HOME_REGION_Y] = OSD.FromUInteger(homeRegionY);
            if (!attributes.ContainsKey(AvatarAttributes.IMAGE_ID)) attributes[AvatarAttributes.IMAGE_ID] = OSD.FromString(dbUser.ProfileImage);
            if (!attributes.ContainsKey(AvatarAttributes.LAST_NAME)) attributes[AvatarAttributes.LAST_NAME] = OSD.FromString(dbUser.LastName);
            if (!attributes.ContainsKey(AvatarAttributes.PARTNER_ID)) attributes[AvatarAttributes.PARTNER_ID] = OSD.FromString(dbUser.Partner);
            if (!attributes.ContainsKey(AvatarAttributes.USER_FLAGS)) attributes[AvatarAttributes.USER_FLAGS] = OSD.FromInteger(dbUser.UserFlags);
            if (!attributes.ContainsKey(AvatarAttributes.WANT_DO)) attributes[AvatarAttributes.WANT_DO] = OSD.FromInteger(dbUser.ProfileWantDoMask);

            // Fetch the appearance for this avatar
            AvatarAppearance dbAppearance = userDatabase.AvatarAppearance.SingleOrDefault(user => user.Owner == dbUser.UUID);
            if (dbAppearance != null)
            {
                if (!attributes.ContainsKey(AvatarAttributes.EYES_ITEM)) attributes[AvatarAttributes.EYES_ITEM] = OSD.FromString(dbAppearance.EyesItem);
                if (!attributes.ContainsKey(AvatarAttributes.GLOVES_ITEM)) attributes[AvatarAttributes.GLOVES_ITEM] = OSD.FromString(dbAppearance.GlovesItem);
                if (!attributes.ContainsKey(AvatarAttributes.HAIR_ITEM)) attributes[AvatarAttributes.HAIR_ITEM] = OSD.FromString(dbAppearance.HairItem);
                if (!attributes.ContainsKey(AvatarAttributes.JACKET_ITEM)) attributes[AvatarAttributes.JACKET_ITEM] = OSD.FromString(dbAppearance.JacketItem);
                if (!attributes.ContainsKey(AvatarAttributes.PANTS_ITEM)) attributes[AvatarAttributes.PANTS_ITEM] = OSD.FromString(dbAppearance.PantsItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHAPE_ITEM)) attributes[AvatarAttributes.SHAPE_ITEM] = OSD.FromString(dbAppearance.BodyItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHIRT_ITEM)) attributes[AvatarAttributes.SHIRT_ITEM] = OSD.FromString(dbAppearance.ShirtItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHOES_ITEM)) attributes[AvatarAttributes.SHOES_ITEM] = OSD.FromString(dbAppearance.ShoesItem);
                if (!attributes.ContainsKey(AvatarAttributes.SKIN_ITEM)) attributes[AvatarAttributes.SKIN_ITEM] = OSD.FromString(dbAppearance.SkinItem);
                if (!attributes.ContainsKey(AvatarAttributes.SKIRT_ITEM)) attributes[AvatarAttributes.SKIRT_ITEM] = OSD.FromString(dbAppearance.SkirtItem);
                if (!attributes.ContainsKey(AvatarAttributes.SOCKS_ITEM)) attributes[AvatarAttributes.SOCKS_ITEM] = OSD.FromString(dbAppearance.SocksItem);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERPANTS_ITEM)) attributes[AvatarAttributes.UNDERPANTS_ITEM] = OSD.FromString(dbAppearance.UnderpantsItem);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERSHIRT_ITEM)) attributes[AvatarAttributes.UNDERSHIRT_ITEM] = OSD.FromString(dbAppearance.UndershirtItem);

                if (!attributes.ContainsKey(AvatarAttributes.EYES_ASSET)) attributes[AvatarAttributes.EYES_ASSET] = OSD.FromString(dbAppearance.EyesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.GLOVES_ASSET)) attributes[AvatarAttributes.GLOVES_ASSET] = OSD.FromString(dbAppearance.GlovesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.HAIR_ASSET)) attributes[AvatarAttributes.HAIR_ASSET] = OSD.FromString(dbAppearance.HairAsset);
                if (!attributes.ContainsKey(AvatarAttributes.JACKET_ASSET)) attributes[AvatarAttributes.JACKET_ASSET] = OSD.FromString(dbAppearance.JacketAsset);
                if (!attributes.ContainsKey(AvatarAttributes.PANTS_ASSET)) attributes[AvatarAttributes.PANTS_ASSET] = OSD.FromString(dbAppearance.PantsAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHAPE_ASSET)) attributes[AvatarAttributes.SHAPE_ASSET] = OSD.FromString(dbAppearance.BodyAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHIRT_ASSET)) attributes[AvatarAttributes.SHIRT_ASSET] = OSD.FromString(dbAppearance.ShirtAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHOES_ASSET)) attributes[AvatarAttributes.SHOES_ASSET] = OSD.FromString(dbAppearance.ShoesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SKIN_ASSET)) attributes[AvatarAttributes.SKIN_ASSET] = OSD.FromString(dbAppearance.SkinAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SKIRT_ASSET)) attributes[AvatarAttributes.SKIRT_ASSET] = OSD.FromString(dbAppearance.SkirtAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SOCKS_ASSET)) attributes[AvatarAttributes.SOCKS_ASSET] = OSD.FromString(dbAppearance.SocksAsset);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERPANTS_ASSET)) attributes[AvatarAttributes.UNDERPANTS_ASSET] = OSD.FromString(dbAppearance.UnderpantsAsset);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERSHIRT_ASSET)) attributes[AvatarAttributes.UNDERSHIRT_ASSET] = OSD.FromString(dbAppearance.UndershirtAsset);

                if (!attributes.ContainsKey(AvatarAttributes.TEXTURE_ENTRY)) attributes[AvatarAttributes.TEXTURE_ENTRY] = OSD.FromBinary(dbAppearance.Texture);
                if (!attributes.ContainsKey(AvatarAttributes.VISUAL_PARAMS)) attributes[AvatarAttributes.VISUAL_PARAMS] = OSD.FromBinary(dbAppearance.VisualParams);
                if (!attributes.ContainsKey(AvatarAttributes.AVATAR_HEIGHT)) attributes[AvatarAttributes.AVATAR_HEIGHT] = OSD.FromReal(dbAppearance.AvatarHeight);
            }

            // Fetch the last starting location for this avatar
            Agents dbAgent = userDatabase.Agents.SingleOrDefault(user => user.UUID == dbUser.UUID);
            if (dbAgent != null)
            {
                Vector3 position, lookAt;
                if (!Vector3.TryParse(dbAgent.CurrentPos, out position))
                    position = new Vector3(128f, 128f, 100f);
                if (!Vector3.TryParse(dbAgent.CurrentLookAt, out lookAt))
                    lookAt = Vector3.UnitZ;

                uint regionX, regionY;
                Utils.LongToUInts((ulong)dbAgent.CurrentHandle, out regionX, out regionY);
                regionX /= 256;
                regionY /= 256;

                if (!attributes.ContainsKey(AvatarAttributes.LAST_LOOKAT)) attributes[AvatarAttributes.LAST_LOOKAT] = OSD.FromVector3(lookAt);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_POSITION)) attributes[AvatarAttributes.LAST_POSITION] = OSD.FromVector3(position);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_ID)) attributes[AvatarAttributes.LAST_REGION_ID] = OSD.FromString(dbAgent.CurrentRegion);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_X)) attributes[AvatarAttributes.LAST_REGION_X] = OSD.FromUInteger(regionX);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_Y)) attributes[AvatarAttributes.LAST_REGION_Y] = OSD.FromUInteger(regionY);
            }
        }
    }
}
